package com.enjoyor.soa.traffic.frame.resolver.impl;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.TypeMismatchException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.HttpMediaTypeNotAcceptableException;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.mvc.multiaction.NoSuchRequestHandlingMethodException;

import com.enjoyor.soa.traffic.frame.resolver.ResultErrorResoler;
import com.enjoyor.soa.traffic.util.bean.error.DError;
import com.enjoyor.soa.traffic.util.exception.RestException;
import com.enjoyor.soa.traffic.util.exception.SignificantRestException;
import com.enjoyor.soa.traffic.util.pojo.ResultPojo;

@SuppressWarnings("deprecation")
public class DefaultResultErrorResolver implements ResultErrorResoler, InitializingBean {

    public static final String DEFAULT_EXCEPTION_MESSAGE = "common.unknownException";
    public static final int DEFAULT_EXCEPTION_CODE = HttpStatus.BAD_REQUEST.value();
    private Map<String, ResultPojo> exceptionMappings = Collections.emptyMap();
    private Map<String, String> exceptionMappingDefinitions = Collections.emptyMap();
    private int defaultExCode = DEFAULT_EXCEPTION_CODE;
    private String defaultExMsg = "server.error";
    private MessageSource messageSource;
    private LocaleResolver localeResolver;

    public void setDefaultExCode(int defaultExCode) {
        this.defaultExCode = defaultExCode;
    }

    public void setDefaultExMsg(String defaultExMsg) {
        this.defaultExMsg = defaultExMsg;
    }

    public void setExceptionMappingDefinitions(Map<String, String> exceptionMappingDefinitions) {
        this.exceptionMappingDefinitions = exceptionMappingDefinitions;
    }

    public LocaleResolver getLocaleResolver() {
        return this.localeResolver;
    }

    public void setLocaleResolver(LocaleResolver localeResolver) {
        this.localeResolver = localeResolver;
    }

    public MessageSource getMessageSource() {
        return this.messageSource;
    }

    public void setMessageSource(MessageSource messageSource) {
        this.messageSource = messageSource;
    }

    public void afterPropertiesSet() throws Exception {
        Map<String, String> definitions = createDefaultExceptionMappingDefinitions();
        if ((this.exceptionMappingDefinitions != null) && (!this.exceptionMappingDefinitions.isEmpty())) {
            definitions.putAll(this.exceptionMappingDefinitions);
        }
        this.exceptionMappings = toResultErrors(definitions);
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected final Map<String, String> createDefaultExceptionMappingDefinitions() {

        Map<String, String> m = new LinkedHashMap();

        applyDefinition(m, HttpMessageNotReadableException.class, HttpStatus.BAD_REQUEST);
        applyDefinition(m, MissingServletRequestParameterException.class, HttpStatus.BAD_REQUEST);
        applyDefinition(m, TypeMismatchException.class, HttpStatus.BAD_REQUEST);
        applyDef(m, "javax.validation.ValidationException", HttpStatus.BAD_REQUEST);

        applyDefinition(m, NoSuchRequestHandlingMethodException.class, HttpStatus.NOT_FOUND);
        applyDef(m, "org.hibernate.ObjectNotFoundException", HttpStatus.NOT_FOUND);

        applyDefinition(m, HttpRequestMethodNotSupportedException.class, HttpStatus.METHOD_NOT_ALLOWED);

        applyDefinition(m, HttpMediaTypeNotAcceptableException.class, HttpStatus.NOT_ACCEPTABLE);

        applyDef(m, "org.springframework.dao.DataIntegrityViolationException", HttpStatus.CONFLICT);

        applyDefinition(m, HttpMediaTypeNotSupportedException.class, HttpStatus.UNSUPPORTED_MEDIA_TYPE);

        return m;
    }

    private void applyDefinition(Map<String, String> m, Class<?> clazz, HttpStatus status) {
        applyDef(m, clazz.getName(), status);
    }

    private void applyDef(Map<String, String> m, String key, HttpStatus status) {
        m.put(key, definitionFor(status));
    }

    private String definitionFor(HttpStatus status) {
        return status.value() + ", " + "_exmsg";
    }

    @SuppressWarnings({ "unchecked", "rawtypes" })
    protected Map<String, ResultPojo> toResultErrors(Map<String, String> smap) {
        if (CollectionUtils.isEmpty(smap)) {
            return Collections.emptyMap();
        }
        Map<String, ResultPojo> map = new LinkedHashMap(smap.size());
        for (Map.Entry<String, String> entry : smap.entrySet()) {
            String key = (String) entry.getKey();
            String value = (String) entry.getValue();
            ResultPojo template = toResultError(value);
            map.put(key, template);
        }
        return map;
    }

    @SuppressWarnings("static-access")
    protected ResultPojo toResultError(String exceptionConfig) {
        String[] values = StringUtils.commaDelimitedListToStringArray(exceptionConfig);
        if ((values == null) || (values.length == 0)) {
            throw new IllegalStateException(
                    "Invalid config mapping.  Exception names must map to a string configuration.");
        }
        DError.ErrorBuilder builder = DError.custom();

        boolean codeSet = false;
        boolean msgSet = false;
        for (String value : values) {
            String trimmedVal = StringUtils.trimWhitespace(value);

            String[] pair = StringUtils.split(trimmedVal, "=");
            if (pair != null) {
                String pairKey = StringUtils.trimWhitespace(pair[0]);
                if (!StringUtils.hasText(pairKey)) {
                    pairKey = null;
                }
                String pairValue = StringUtils.trimWhitespace(pair[1]);
                if (!StringUtils.hasText(pairValue)) {
                    pairValue = null;
                }
                if ("code".equalsIgnoreCase(pairKey)) {
                    int code = getRequiredInt(pairKey, pairValue);
                    builder.setCode(code);
                    codeSet = true;
                } else if ("msg".equalsIgnoreCase(pairKey)) {
                    builder.setMessage(pairValue);
                    msgSet = true;
                }
            } else {
                if (!codeSet) {
                    int val = getInt("code", trimmedVal);
                    if (val > 0) {
                        builder.setCode(val);
                        codeSet = true;
                        continue;
                    }
                }
                if (!msgSet) {
                    builder.setMessage(trimmedVal);
                    msgSet = true;
                }
            }
        }

        return new ResultPojo().buildError(builder.build());

    }

    @SuppressWarnings("static-access")
    public ResultPojo resolveError(ServletWebRequest request, Object handler, Exception ex) {
        ResultPojo template = getErrorTemplate(ex);
        if (template == null) {
            throw ((RuntimeException) ex);
        }
        String msg = getMessage(template, request, ex);
        int code = getCode(template, request, ex);

        DError.ErrorBuilder builder = DError.custom().setMessage(msg).setCode(code);

        Throwable[] throwables = ExceptionUtils.getThrowables(ex);
        if ((throwables != null) && (throwables.length > 0)) {
            for (Throwable throwable : throwables) {
                builder.addError(new DError.IError(getMessage("_exmsg", request, throwable),
                        getCode(template, request, throwable)));
            }
        }

        return new ResultPojo().buildError(builder.build());
    }

    protected String getMessage(ResultPojo template, ServletWebRequest request, Throwable ex) {
        return getMessage(template.getDataBuffer(), request, ex);
    }

    protected String getMessage(String msg, ServletWebRequest webRequest, Throwable ex) {
        if (msg != null) {
            if ((msg.equalsIgnoreCase("null")) || (msg.equalsIgnoreCase("off"))) {
                msg = this.defaultExMsg;
            }
            if (msg.equalsIgnoreCase("_exmsg")) {
                msg = ex.getMessage();
                if (msg == null) {
                    msg = ex.getClass().getName();
                }
            }
        } else {
            msg = this.defaultExMsg;
        }
        if (this.messageSource != null) {
            Locale locale = null;
            if (this.localeResolver != null) {
                locale = this.localeResolver.resolveLocale(webRequest.getRequest());
            }
            msg = this.messageSource.getMessage(msg, null, msg, locale);
        }
        return msg;
    }

    protected int getCode(ResultPojo template, ServletWebRequest request, Throwable ex) {
        int code = Integer.valueOf(template.getAppCode());
        if (code <= 0) {
            if ((ex instanceof RestException)) {
                code = ((RestException) ex).status.value();
            } else if ((ex instanceof SignificantRestException)) {
                code = ((SignificantRestException) ex).getStatus().value();
            } else {
                code = this.defaultExCode;
            }
        }
        return code;
    }

    private ResultPojo getErrorTemplate(Throwable ex) {
        Map<String, ResultPojo> mappings = this.exceptionMappings;
        if (CollectionUtils.isEmpty(mappings)) {
            return null;
        }
        ResultPojo template = null;
        int deepest = Integer.MAX_VALUE;
        for (Map.Entry<String, ResultPojo> entry : mappings.entrySet()) {
            String key = (String) entry.getKey();
            int depth = getDepth(key, ex);
            if ((depth >= 0) && (depth < deepest)) {
                deepest = depth;
                template = (ResultPojo) entry.getValue();
            }
        }
        return template;
    }

    protected int getDepth(String exceptionMapping, Throwable ex) {
        return getDepth(exceptionMapping, ex.getClass(), 0);
    }

    private int getDepth(String exceptionMapping, Class<?> exceptionClass, int depth) {
        if (exceptionClass.getName().contains(exceptionMapping)) {
            return depth;
        }
        if (exceptionClass.equals(Throwable.class)) {
            return -1;
        }
        return getDepth(exceptionMapping, exceptionClass.getSuperclass(), depth + 1);
    }

    private static int getRequiredInt(String key, String value) {
        try {
            int anInt = Integer.valueOf(value).intValue();
            return Math.max(-1, anInt);
        } catch (NumberFormatException e) {
            String msg = "Configuration element '" + key + "' requires an integer value.  The value specified: "
                    + value;
            throw new IllegalArgumentException(msg, e);
        }
    }

    private static int getInt(String key, String value) {
        try {
            return getRequiredInt(key, value);
        } catch (IllegalArgumentException iae) {
        }
        return 0;
    }

}
